Skip to content

feat(tab): add H/P text labels on hammer-on / pull-off arcs#2609

Open
rafaelsales wants to merge 3 commits intoCoderLine:developfrom
rafaelsales:feature/issue-2608
Open

feat(tab): add H/P text labels on hammer-on / pull-off arcs#2609
rafaelsales wants to merge 3 commits intoCoderLine:developfrom
rafaelsales:feature/issue-2608

Conversation

@rafaelsales
Copy link
Copy Markdown
Contributor

@rafaelsales rafaelsales commented Mar 23, 2026

Summary

Fixes #2608.

In the Tab (and TabMixed) renderer, hammer-on and pull-off connections are drawn as arcs between notes but the conventional H / P text labels were never rendered.

This PR adds H/P label rendering and correctly handles edge cases like H/P chains and multiple H/P on the same beat.

Changes

Commit 1feat(tab): add H/P text labels to hammer-on and pull-off arcs

  • TieGlyph.ts — adds a getSlurText() virtual method (returns undefined by default) and draws the label text at the arc midpoint in paint() when present.
  • TabSlurGlyph.ts — accepts an optional slurText?: string constructor parameter and overrides getSlurText() to return it.
  • TabBeatContainerGlyph.ts — passes 'H' or 'P' when creating effect slurs for hammer-pull origin notes.

Commit 2fix(tab): handle H/P chain and same-beat edge cases

  • TabSlurGlyph.tstryExpand() now accepts an optional slurText parameter and rejects merging slurs with different labels (prevents label loss when multiple H/P on same beat share beam direction).
  • TabBeatContainerGlyph.ts — creates individual arcs per H/P pair using hammerPullOrigin/hammerPullDestination links (instead of the collapsed effectSlurOrigin/effectSlurDestination). This renders chains like 5{h} 7{h} 5 as two separate arcs (H then P) instead of one collapsed arc. Non-H/P effect slurs (e.g. legato slides) continue to use the existing effectSlur path, guarded by !n.isHammerPullOrigin / !n.isHammerPullDestination.

How it looks

hp-labels-screenshot

Test plan

  • Simple hammer-on (ascending fret) → "H" label
  • Simple pull-off (descending fret) → "P" label
  • H then P in same bar → separate arcs with correct labels
  • H and P on different strings → H above, P below
  • H/P chain (5{h} 7{h} 5) → individual H and P arcs
  • Long chain (5{h} 7{h} 5{h} 7) → alternating H/P/H arcs
  • Legato slide ({sl}) → arc without label (regression check)
  • Plain notes → no arcs, no labels (regression check)
Running the local test page

Step 1 — Save the HTML below as test-hp-labels.html in the repo root.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>AlphaTab – Hammer-on / Pull-off H/P Labels</title>
    <style>
        body { font-family: sans-serif; max-width: 900px; margin: 40px auto; padding: 0 20px; background: #fff; }
        h2 { margin-top: 32px; font-size: 1em; color: #333; }
        p { color: #555; font-size: 0.9em; }
        .at-wrap { border: 1px solid #ddd; border-radius: 4px; margin-top: 8px; }
        .at-footer { display: none; }
    </style>
</head>
<body>
    <h1>Hammer-on / Pull-off H/P Labels</h1>
    <p>
        Each section renders a different H/P scenario in the Tab stave profile.
        Each arc should display an <strong>H</strong> or <strong>P</strong> label above it.
    </p>

    <h2>1. Simple hammer-on (5→7 on string 3)</h2>
    <div id="at1" class="at-wrap"></div>

    <h2>2. Simple pull-off (7→5 on string 3)</h2>
    <div id="at2" class="at-wrap"></div>

    <h2>3. Hammer-on then pull-off (5→7, 7→5) in the same bar</h2>
    <div id="at3" class="at-wrap"></div>

    <h2>4. H and P on different strings (string 3 H, string 4 P)</h2>
    <div id="at4" class="at-wrap"></div>

    <h2>5. H/P chain (5→7→5) — individual H then P arcs</h2>
    <div id="at5" class="at-wrap"></div>

    <h2>6. Long chain (5→7→5→7) — alternating H/P arcs</h2>
    <div id="at6" class="at-wrap"></div>

    <h2>7. Legato slide — arc without H/P label</h2>
    <div id="at7" class="at-wrap"></div>

    <h2>8. Plain notes — no arc, no label</h2>
    <div id="at8" class="at-wrap"></div>

    <script src="packages/alphatab/dist/alphaTab.js"></script>
    <script>
        const apis = [];
        const settings = {
            core: {
                engine: 'html5',
                fontDirectory: 'packages/alphatab/dist/font/',
                logLevel: 0,
            },
            display: {
                staveProfile: 'Tab',
                scale: 1.5,
            },
            notation: {
                elements: {
                    guitarTuning: false,
                    trackNames: false,
                }
            },
            player: { enablePlayer: false },
        };

        function renderTex(containerId, tex) {
            const api = new alphaTab.AlphaTabApi(
                document.getElementById(containerId),
                settings
            );
            api.tex(tex, [0]);
            apis.push(api);
        }

        const queue = [
            ['at1', ':4 5.3{h} 7.3 r r'],
            ['at2', ':4 7.3{h} 5.3 r r'],
            ['at3', ':4 5.3{h} 7.3 7.3{h} 5.3'],
            ['at4', ':4 5.3{h} 7.3 8.4{h} 5.4'],
            ['at5', ':4 5.3{h} 7.3{h} 5.3 r'],
            ['at6', ':8 5.3{h} 7.3{h} 5.3{h} 7.3 r r r r'],
            ['at7', ':4 5.3{sl} 7.3 r r'],
            ['at8', ':4 5.3 7.3 5.3 7.3'],
        ];

        queue.forEach(([id, tex]) => {
            renderTex(id, tex);
        });
    </script>
</body>
</html>

Step 2 — Build the web bundle and start a local server from the repo root:

npm install
npm run build-web
python3 -m http.server 8765

Step 3 — Open http://localhost:8765/test-hp-labels.html in your browser.

@rafaelsales rafaelsales force-pushed the feature/issue-2608 branch 3 times, most recently from f23e3bd to 6901cd3 Compare March 23, 2026 03:41
@Danielku15
Copy link
Copy Markdown
Member

Great addition, thanks for the change. We might need to check some additional edge cases like:

  • Having multiple hammer-on/pull-offs on the same beat.
  • Having Direct hammer-on/pull-off chains

@rafaelsales
Copy link
Copy Markdown
Contributor Author

Thanks for the review! I've pushed a follow-up commit addressing both edge cases:

1. Direct H/P chains

The model (Note.finish()) collapses effect slur chains into a single arc from first to last note. For 5{h} 7{h} 5, this produced one arc labeled "H" instead of two separate arcs.

Fix: The renderer now uses hammerPullOrigin/hammerPullDestination links directly (instead of the collapsed effectSlurOrigin/effectSlurDestination) to create individual arcs per H/P pair, each with the correct label. Non-H/P effect slurs (legato slides) continue using the existing effectSlur path.

2. Multiple H/P on the same beat

When two notes on the same beat pair had H/P effects with the same beam direction, tryExpand() merged them into one slur, silently discarding the second label.

Fix: tryExpand() now accepts an optional slurText parameter and rejects merging when the labels differ.

Both are covered by new test cases in the test page (sections 5 and 6).

In the Tab renderer, the arc connecting hammer-on and pull-off notes
is now annotated with an "H" (ascending fret = hammer-on) or "P"
(descending fret = pull-off) label above the arc midpoint.

The label is drawn via an overridden paint() in TabSlurGlyph, reusing
the same canvas.fillText path already used for whammy/bend slurText.
TieGlyph's coordinate fields (_startX/Y, _endX/Y, _tieHeight,
_shouldPaint) are widened from private to protected to allow the
subclass to read them during paint.

Fixes CoderLine#2608
- Individual arcs per H/P pair in chains (e.g. 5{h} 7{h} 5 now
  renders separate H and P arcs instead of one collapsed arc)
- Prevent tryExpand from merging slurs with different H/P labels
  (fixes label loss when multiple H/P on same beat share beam direction)
- Guard existing effectSlur blocks to skip H/P notes, keeping legato
  slide rendering unaffected
@Danielku15
Copy link
Copy Markdown
Member

While testing and extending things to get this integrated I noticed that some parts break when things are combined in legato slides and hammer ons.

Before:
image
After:
image

Then I checked how other apps like GP and Dorico handle it, and they seem to have a single tie (arc) with multiple labels:

image image

I think we should follow the same display. It feels less noisy and nicely annotates intermediate parts. but this will require some more implementation to even support such things:

  1. We would extend the slur/tie as before across multiple effects
  2. On the tie/slur creation, and during expansion we additionally add the slur text for the effect.
  3. Internally the slur/tie would need to keep a list of labels with their respective start and end notes.
  4. The tie/slur layout would then need to calculate the x-positions for all labels
  5. During tie/slur drawing we would draw them all.

@rafaelsales Id you like you can give it a try, otherwise I'd keep this PR open until I can look deeper into building this slur/tie label foundation.

@Danielku15 Danielku15 force-pushed the feature/issue-2608 branch from aa535df to 6d996f1 Compare May 2, 2026 14:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tab renderer: Hammer-on / Pull-off arcs missing H/P text labels

2 participants